using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Windows.Controls;
using WindowsPhoneEssentials.Diagnostics;
using WindowsPhoneEssentials.Threading;
using WindowsPhoneMVC.ActionResults;
using WindowsPhoneMVC.Framework.Interfaces;
using WindowsPhoneMVC.Interfaces;
using WindowsPhoneMVC.Navigation.Interfaces;
using WindowsPhoneMVC.Phone;
using WindowsPhoneMVC.Phone.Interfaces;

namespace WindowsPhoneMVC.Navigation
{
    public class Navigator : INavigator
    {
        private readonly IControllerLocator controllerLocator;
        private readonly INavigationContextFactory navigationContextFactory;
        private readonly IBackgroundWorker backgroundWorker;
        private NavigationContext currentContext;
        private IJournal navigationJournal;
        private ActionResult lastView;
        private readonly List<PartialViewResult> partialViewsToCleanup = new List<PartialViewResult>(); 
        private int navigationThreadId;
        private readonly List<int> cancellationRequests = new List<int>();
        private Action<object> currentResultCallback;
        private bool doNotRestoreAfterCallback;
        private readonly IControllerFactory controllerFactory;
        private readonly IActionResolver actionResolver;

        /// <summary>
        /// Initializes a new instance of the <see cref="Navigator"/> class.
        /// </summary>
        /// <param name="controllerLocator">Controller locator.</param>
        /// <param name="navigationContextFactory">The Navigation Context Factory.</param>
        /// <param name="backgroundWorker">Background worker</param>
        /// <param name="controllerFactory">Controller Factory</param>
        /// <param name="actionResolver">Action Resolver</param>
        public Navigator(
            IControllerLocator controllerLocator,
            INavigationContextFactory navigationContextFactory, 
            IBackgroundWorker backgroundWorker,
            IControllerFactory controllerFactory,
            IActionResolver actionResolver)
        {
            this.controllerLocator = controllerLocator;
            this.navigationContextFactory = navigationContextFactory;
            this.backgroundWorker = backgroundWorker;
            this.controllerFactory = controllerFactory;
            this.actionResolver = actionResolver;
        }

        public virtual void Initialise(IJournal journal, IMvcFrame frame, string frameName)
        {
            navigationJournal = journal;
            NavigationFrameName = frameName;
            NavigationFrame = frame;
        }

        public void RefreshCurrentView()
        {
            Invoke(navigationJournal.PopEntry());
        }

        public virtual void RemoveBackEntry()
        {
            navigationJournal.PopEntry();
        }

        /// <summary>
        /// Gets or sets navigation frame used by this navigator.
        /// </summary>
        /// <value>Navigation frame.</value>
        public virtual IMvcFrame NavigationFrame { get; private set; }

        /// <summary>
        /// Gets name of navigation frame used by this navigator.
        /// </summary>
        /// <value>Name of navigation frame.</value>
        public virtual string NavigationFrameName { get; private set; }

        public virtual bool CancelNavigation()
        {
            lock (typeof(NavigationLock))
            {
                cancellationRequests.Add(navigationThreadId);
                NavigationFrame.OnNavigationCompleted(false);
                if (navigationJournal.IsEmpty())
                    return false;
            }

            OnNavigationCancelled(EventArgs.Empty);
            return true;
        }

        /// <summary>
        /// Occurs when navigation is canceled.
        /// </summary>
        public virtual event EventHandler NavigationCancelled;

        public void NavigateBackTo(string controllerName, string actionName)
        {
            var poppedRequests = new List<NavigationRequest>();

            var request = navigationJournal.PopEntry();
            poppedRequests.Add(request);

            while (request != null &&
                request.ControllerName.Replace("Controller", string.Empty) != controllerName &&
                DefaultViewLocator.RemoveSuffixes(request.ActionName) != DefaultViewLocator.RemoveSuffixes(actionName))
            {
                request = navigationJournal.PopEntry();
                poppedRequests.Add(request);
            }

            if (request == null)
            {
                var inBackStack = new StringBuilder();
                foreach (var r in poppedRequests.Where(r => r != null))
                {
                    var navigationRequest = r;
                    navigationJournal.PushEntry(r);
                    inBackStack.AppendLine(string.Format("{0}.{1}", navigationRequest.ControllerName, navigationRequest.ActionName));
                }
                Trace.WriteError(this, () => string.Format("Unable to navigate back to {0}.{1}. Found: {0}", controllerName, actionName));
                return;
            }

            Invoke(request, NavigationDirection.Backward);
        }

        public virtual void SaveState()
        {
            if (navigationJournal == null)
            {
                Trace.WriteWarning(this, ()=>"Navigation Journal was null when trying to flush state. App not loaded before exited?");
                return;
            }
            navigationJournal.Flush();
        }

        protected virtual void OnNavigationCancelled(EventArgs e)
        {
            var handler = NavigationCancelled;
            if (handler != null) handler(this, e);
        }

        public virtual void Invoke(NavigationRequest navigationRequest, NavigationDirection direction = NavigationDirection.Forward)
        {
            Navigate(navigationRequest, direction, null);
        }

        public void InvokeBackground(NavigationRequest navigationRequest)
        {
            ValidateNavigationRequest(navigationRequest, NavigationDirection.Forward);
            Execute.OnUIThreadSync(() => NavigationFrame.BeginBackgroundOperation());

            var controllerType = controllerLocator.Locate(navigationRequest.ControllerName);

            backgroundWorker.DoWork(() => Navigate(navigationRequest, controllerType),
                                    r =>
                                    {
                                        //if (!r.HadError)
                                        //    using (TraceScope.Create(this, TraceLevel.Debug, () => string.Format("Executing {0} completion steps", r.Result.GetType())))
                                        //        r.Result.NavigationComplete();

                                        //loadComplete(!r.HadError, r.Result != null ? r.Result.View : null);
                                        //partialViewTraceScope.Dispose();
                                        NavigationFrame.EndBackgroundOperation();
                                    });
        }

        public void InvokePartial(NavigationRequest navigationRequest, Action<bool, UserControl> loadComplete)
        {
            ValidateNavigationRequest(navigationRequest, NavigationDirection.Forward);

            var partialViewTraceScope = TraceScope.Create(this, TraceLevel.Info, () => string.Format("Loading partial view {0}.{1}", navigationRequest.ControllerName, navigationRequest.ActionName));

            Execute.OnUIThreadSync(() => NavigationFrame.BeginBackgroundOperation());

            var controllerType = controllerLocator.Locate(navigationRequest.ControllerName);

            backgroundWorker.DoWork(() => NavigatePartial(navigationRequest, controllerType),
                                    r =>
                                        {
                                            if (!r.HadError)
                                                using (TraceScope.Create(this, TraceLevel.Debug, () => string.Format("Executing {0} completion steps", r.Result.GetType())))
                                                    r.Result.NavigationComplete();

                                            loadComplete(!r.HadError, r.Result != null ? r.Result.View : null);
                                            partialViewTraceScope.Dispose();
                                            NavigationFrame.EndBackgroundOperation();
                                        });
        }

        private PartialViewResult NavigatePartial(NavigationRequest request, Type controllerType)
        {
            currentContext = navigationContextFactory.Create(this, request);

            Controller controller = null;
            ActionResult result;
            try
            {
                controller = controllerFactory.Create(controllerType);
                controller.Initialise(currentContext);
                var action = actionResolver.ResolveControllerAction(controllerType, request);

                using (TraceScope.Create(this, TraceLevel.Info, () => string.Format("Invoking controller action for {0}.{1} (user code)", request.ControllerName, request.ActionName)))
                    result = action(controller);
            }
            finally
            {
                if (controller != null)
                    controllerFactory.Release(controller);
            }

            var partialResult = result as PartialViewResult;

            if (partialResult == null)
                throw new InvalidOperationException("InvokePartial must return a PartialViewResult");

            if (request.PageState == null)
                request.PageState = new PageTransientStore();

            partialResult.Execute();
            partialViewsToCleanup.Add(partialResult);

            return partialResult;
        }

        public void InvokeWithResult(NavigationRequest navigationRequest, Action<object> completeCallback)
        {
            Navigate(navigationRequest, NavigationDirection.Forward, completeCallback);
        }

        public void NavigationResult(object result)
        {
            lock (typeof(NavigationLock))
            {
                if (currentResultCallback == null)
                    throw new InvalidOperationException("Navigator not waiting for any results. Make sure you navigate using InvokeWithResult before using this method");

                if (doNotRestoreAfterCallback)
                {
                    currentResultCallback(result);
                    currentResultCallback = null;                    
                }
                else
                {
                    Execute.OnUIThread(() => NavigationFrame.OnNavigationStarted(NavigationDirection.Backward));
                    //Show last view again (this will restore it, not reinvoke the controller action)
                    backgroundWorker.DoWork(lastView.Execute, r =>
                    {
                        currentResultCallback(result);
                        NavigationFrame.OnNavigationCompleted(true);
                        currentResultCallback = null;
                    });
                }

                doNotRestoreAfterCallback = false;
            }
        }

        private void Navigate(NavigationRequest request, NavigationDirection direction, Action<object> completeCallback)
        {
            lock (typeof(NavigationLock))
            {
                ValidateNavigationRequest(request, direction);

                currentResultCallback = completeCallback;

                Trace.WriteInfo(this, () => string.Format("Memory usage before navigation: {0}", Information.GetMemoryUsage()));
                if (NavigationFrame.IsNavigationInProgress)
                {
                    Trace.WriteWarning(this, () => string.Format("Navigation is already in progress, ignoring navigation request to {0}.{1}", request.ControllerName, request.ActionName));
                    return;
                }
                Execute.OnUIThread(() => NavigationFrame.OnNavigationStarted(direction));

                var traceScope = TraceScope.Create(this, TraceLevel.Info, () => string.Format("Navigating {0} to {1}.{2}", direction, request.ControllerName, request.ActionName));
                var controllerType = controllerLocator.Locate(request.ControllerName);

                backgroundWorker.DoWork(() => Navigate(request, controllerType), result =>
                                                                                     {
                                                                                         traceScope.Dispose();
                                                                                         CompleteNavigation(result);
                                                                                         if (PhoenixApplicationService.StartupScope != null && !NavigationFrame.IsNavigationInProgress)
                                                                                         {
                                                                                             Trace.WriteDebug(this, ()=>"Application Startup Complete");
                                                                                             PhoenixApplicationService.StartupScope.Dispose();
                                                                                             PhoenixApplicationService.StartupScope = null;
                                                                                         }
                                                                                     });
            }
        }

        private NavigationCompleteResult Navigate(NavigationRequest request, Type controllerType)
        {
            var viewModelType = typeof (ViewModelBase);
            if (request.Parameters.Any(p => p.Value != null && viewModelType.IsAssignableFrom(p.Value.GetType())))
                Trace.WriteWarning(this, ()=>string.Format("Passing ViewModels as parameters may cause memory pressure as they are not disposed! \r\vConsider using base type of NotifyPropertyChanged if the parameter is actually an entity, not a ViewModel."));

            navigationThreadId = Thread.CurrentThread.ManagedThreadId;

            currentContext = navigationContextFactory.Create(this, request);

            Controller controller = null;
            ActionResult result;
            try
            {
                controller = controllerFactory.Create(controllerType);
                controller.Initialise(currentContext);
                var action = actionResolver.ResolveControllerAction(controllerType, request);

                using (TraceScope.Create(this, TraceLevel.Info, () => string.Format("Invoking controller action for {0}.{1} (user code)", request.ControllerName, request.ActionName)))
                    result = action(controller);
            }
            finally
            {
                if (controller != null)
                    controllerFactory.Release(controller);
            }

            if (WasCancelled())
            {
                Trace.WriteInfo(this, ()=> string.Format("Navigation to {0}.{1} cancelled", request.ControllerName, request.ActionName));
                return new NavigationCompleteResult{Cancelled = true};
            }

            if (result is ViewResult && request.PageState == null)
            {
                request.PageState = new PageTransientStore();
            }

            using (TraceScope.Create(this, TraceLevel.Debug, () => string.Format("Executing {0} for {1}.{2}", result.GetType(), request.ControllerName, request.ActionName)))
                result.Execute();

            //Don't dispose last view if we have a callback
            if (currentResultCallback == null && result is PageResult)
            {
                navigationJournal.PushEntry(request);
                var disposable = lastView as IDisposable;
                if (disposable != null)
                    disposable.Dispose();
                foreach (var partialView in partialViewsToCleanup)
                    partialView.Dispose();
                partialViewsToCleanup.Clear();

                lastView = result;
            }

            if (result is DataResult)
            {
                doNotRestoreAfterCallback = true;
            }

            return new NavigationCompleteResult(result);
        }

        private void CompleteNavigation(BackgroundResult<NavigationCompleteResult> r)
        {
            if (r.Result != null && r.Result.Cancelled) return;
            if (r.HadError)
            {
                //TODO Not sure about error handling.. Make decision
                Trace.WriteError(this, ()=> string.Format("MVC Navigation Failed: {0}\r\nDetails: {1}", r.Error.Message, r.Error));
            }

            NavigationFrame.OnNavigationCompleted(!r.HadError);
            if (!r.HadError && r.Result != null && r.Result.Result is IHaveNavigationCompletedSteps)
            {
                Trace.WriteDebug(typeof(Navigator), () => string.Format("Executing {0} completion steps", r.Result.Result.GetType()));
                ((IHaveNavigationCompletedSteps)r.Result.Result).NavigationComplete();
            }

            Trace.WriteInfo(this, ()=>string.Format("Memory usage after navigation: {0}", Information.GetMemoryUsage()));
        }

        private bool WasCancelled()
        {
            lock (typeof(NavigationLock))
            {
                var managedThreadId = Thread.CurrentThread.ManagedThreadId;
                if (cancellationRequests.Contains(managedThreadId))
                {
                    cancellationRequests.Remove(managedThreadId);
                    return true;
                }
                return false;
            }
        }

        public virtual bool NavigateBack()
        {
            lock (typeof(NavigationLock))
            {
                Trace.WriteInfo(this, ()=>"Navigating back");
                if (currentResultCallback != null)
                {
                    Execute.OnUIThread(() => NavigationFrame.OnNavigationStarted(NavigationDirection.Backward));
                    backgroundWorker.DoWork(lastView.Execute, r => NavigationFrame.OnNavigationCompleted(true));
                    currentResultCallback = null;
                    return true;
                }

                var lastRequest = navigationJournal.PopEntry();
                if (lastRequest == null)
                    return false;

                var prevRequest = navigationJournal.PopEntry();
                if (prevRequest == null)
                    return false;

                Invoke(prevRequest, NavigationDirection.Backward);

                return true;
            }
        }

        private void ValidateNavigationRequest(NavigationRequest request, NavigationDirection direction)
        {
            if (request == null) throw new ArgumentNullException("request");
            if (request.ActionName == null) throw new ArgumentNullException("request", "request.ActionName cannot be null");
            if (request.ControllerName == null) throw new ArgumentNullException("request", "request.ControllerName cannot be null");
            if (direction == NavigationDirection.Forward && currentResultCallback != null)
                throw new InvalidOperationException("You cannot navigate forward while waiting for a result (from InvokeWithResult). Navigate back, or call Navigator.NavigationResult first");
        }
    }
}